/**
 *    Copyright (C) 2007-2009 DIVIS GmbH (http://www.divis-hagen.de/)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 3 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General
 *    Public License along with this library; if not, write to the
 *    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *    Boston, MA 02110-1301 USA
 */
package de.divis_hagen.json.rs.client;

import de.divis_hagen.json.JSONContextImpl;
import de.divis_hagen.json.JSONException;
import de.divis_hagen.json.RelationType;
import de.divis_hagen.json.TypeMeta;
import de.divis_hagen.json.annotation.JSONCollectionResource;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;


/**
 * <p>
 * Create JavaScript-client-files from JAX-RS and JAXB classes.
 * </p>
 *
 * @author Aike J Sommer
 */
public class JSCompiler {

    private final String basePackage;

    private final boolean debug;

//    private final Set<Class> handlerClasses = new HashSet<Class>();
    private final Set<Class> entityClasses = new HashSet<Class>();

    private final Map<TypeMeta, ResourceInfo> entityInfos = new HashMap<TypeMeta, ResourceInfo>();

    private final JSONContextImpl context;

//    private final TreeBuilder builder;
    private int indent = 0;

    private Writer out;

    private final String jsPath;

    private final String apiPath;

    public JSCompiler(String basePackage, Collection<Class> entityClasses,
            String jsPath, String apiPath, boolean debug) {
        this.basePackage = basePackage;
        this.debug = debug;
        this.entityClasses.addAll(entityClasses);
        this.jsPath = jsPath;
        this.apiPath = apiPath;

        context = new JSONContextImpl(entityClasses.toArray(new Class[0]));
//        builder = new TreeBuilder(context);

        resolveClasses();
    }

    private void resolveClasses() {
        for (Class c : entityClasses) {
            if (c.isAnnotationPresent(JSONCollectionResource.class)) {
//                EntityType type = (EntityType) c.getAnnotation(EntityType.class);
                ResourceInfo info = new ResourceInfo(context, c, basePackage);
                entityInfos.put(info.getElement(), info);
            }
        }
    }

//    public TreeBuilder builder() {
//        return builder;
//    }
//    public void generate(OutputStream out) throws IOException, JSONException {
//        generate(new OutputStreamWriter(out));
//    }
    public void generate(File dir) throws IOException, JSONException {
//        Collection<EntityInfo> entityInfos = loadInfo();

        indent = 0;
//        this.out = out;
//        generateIntro();

        new File(dir, basePackage).mkdir();
        for (ResourceInfo entity : entityInfos.values()) {
            File target = new File(dir, basePackage + "/" + entity.getElement().getName() + ".js");
            out = new FileWriter(target);

            generateEntityType(entity.getElement(),
                    entity.getCollectionPath());
            generateHandlerType(entity);

            out.close();
            out = null;
        }
        for (TypeMeta.Primitive enumType : context.getEnumTypes()) {
            File target = new File(dir, basePackage + "/" + enumType.getName() + ".js");
            out = new FileWriter(target);

            generateEnumType(enumType);

            out.close();
            out = null;
        }
        for (TypeMeta.Complex complexType : context.getComplexTypes()) {
            if (entityInfos.keySet().contains(complexType) || complexType.isCollectionResource()) {
                continue;
            }

            File target = new File(dir, basePackage + "/" + complexType.getName() + ".js");
            out = new FileWriter(target);

            generateEntityType(complexType, null);

            out.close();
            out = null;
        }

        File target = new File(dir, basePackage + ".js");
        out = new FileWriter(target);

        generateBootstrapIntro();

//        for (ResourceInfo entity : entityInfos.values()) {
//            generateLoadTag(entity.getSimpleName());
//        }
        for (TypeMeta.Complex complexType : context.getComplexTypes()) {
            if (complexType.isCollectionResource()) {
                continue;
            }
            generateLoadTag(complexType.getName());
        }
        for (TypeMeta.Primitive enumType : context.getEnumTypes()) {
            generateLoadTag(enumType.getName());
        }

        generateBootstrapOutro();

        generateInitIntro();

        for (ResourceInfo entity : entityInfos.values()) {
            generateHandlerInstance(entity);
        }

        generateInitOutro();
//        generateOutro();

        out.close();
        out = null;
//        out.flush();
    }

//    private Collection<EntityInfo> loadInfo() {
//        Set<EntityInfo> result = new HashSet<EntityInfo>();
//        for (Map.Entry<Class, Class> entry : entityClasses.entrySet()) {
//            result.add(new EntityInfo(entry.getValue(), entry.getKey(), basePackage, builder));
//        }
//        return result;
//        return entityInfos.values();
//    }
    private void generateEnumType(TypeMeta.Primitive enumType) throws IOException {
        printLine("var " + enumType.getName() + " = {");
        indent++;
        for (String value : enumType.getEnumValues()) {
            printLine(value + " : '" + value + "',");
        }
        printLine("__end : null");
        indent--;
        printLine("}");
    }

    private void printLine() throws IOException {
        printLine("");
    }

    private void printLine(String line) throws IOException {
        for (int i = 0; i < indent; i++) {
            out.write("  ");
        }
        out.write(line + "\n");
    }

//    private void generateIntro() throws IOException {
//        printLine();
//    }
//
//    private void generateOutro() throws IOException {
//    }
    private void generateBootstrapIntro() throws IOException {
        printLine("if (! window['" + basePackage + "']) {");
        printLine();
        indent++;

        printLine("function _" + basePackage + "_include(root, resource) {");
        indent++;
        String name = "tag";
        printLine("var " + name + " = document.createElement('script');");
        printLine(name + ".type = 'text/javascript';");
        printLine(name + ".src = root + '" + jsPath + "/" + basePackage + "/' + resource + '.js';");
        printLine("document.getElementsByTagName('head')[0].appendChild(" + name + ");");
        indent--;
        printLine("}");
        printLine();

        printLine("function _" + basePackage + "_bootstrap() {");
        indent++;
        printLine("var rootLink = document.getElementById(\"" + basePackage + "_ROOT\");");
        printLine("var root = rootLink.href;");
        printLine("if (root[root.length - 1] == '/') {");
        printLine("  root = root.substring(0, root.length - 1);");
        printLine("}");
        printLine();
    }

    private void generateLoadTag(String entityName) throws IOException {
//        String name = "loadTag_" + entityName;
//        printLine("var " + name + " = document.createElement('script');");
//        printLine(name + ".type = 'text/javascript';");
//        printLine(name + ".src = root + '" + jsPath + "/" + basePackage + "/" + entityName
//                + ".js';");
//        printLine("document.getElementsByTagName('head')[0].appendChild(" + name + ");");
//        printLine();
        printLine("_" + basePackage + "_include(root, '" + entityName + "');");
    }

    private void generateBootstrapOutro() throws IOException {
        printLine();
        printLine("var initTag = document.createElement('script');");
        printLine("initTag.type = 'text/javascript';");
        printLine("initTag.text = '_" + basePackage + "_init(\"' + root + '\");';");
        printLine("document.getElementsByTagName('head')[0].appendChild(initTag);");
        indent--;
        printLine("}");
        printLine();
    }

    private void generateInitIntro() throws IOException {
        printLine("function _" + basePackage + "_init(root) {");
        indent++;
        printLine(basePackage + " = {");
        indent++;
        printLine();
        printLine("_root : root,");
        printLine();
        printLine("/**");
        printLine(" * @param {String} resource");
        printLine(" */");
        printLine("include : function (resource) {");
        printLine("  _" + basePackage + "_include(MAF._root, resource);");
        printLine("},");
        printLine();
    }

    private void generateInitOutro() throws IOException {
        printLine("__end : null");
        printLine();
        indent--;
        printLine("}");
        indent--;
        printLine("}");
        printLine();
        printLine("_MAF_bootstrap();");
        printLine();
        indent--;
        printLine("}");
        printLine();
    }

//    private ElementRelation getRelation(ResourceInfo entity, String name) {
//        Type fieldType = entity.getType(name);
//        Class fieldClass = getRaw(fieldType);
//
//        if (fieldClass != null) {
//            Type realType;
//            ElementRelation.RelationType relationType;
//            if (Collection.class.isAssignableFrom(fieldClass)) {
//                Type[] collectionTypes = getTypes(fieldType, Collection.class);
//                if (collectionTypes != null && collectionTypes.length > 0) {
//                    realType = collectionTypes[0];
//                } else {
//                    realType = Object.class;
//                }
//                relationType = ElementRelation.RelationType.Collection;
//            } else {
//                realType = fieldType;
//                relationType = ElementRelation.RelationType.Single;
//            }
//
//            if ((realType instanceof Class) && entityInfos.containsKey((Class) realType)) {
//                return new ElementRelation(entityInfos.get((Class) realType), relationType);
//            }
//        }
//
//        return null;
//    }
    private void generateEntityType(TypeMeta.Complex<Object> meta,
            String collectionPath) throws IOException, JSONException {
//        JSONTree tree = builder.build(entity.getEntity());
//        JSONObject jObj = entity.getJSONObject();//(JSONObject) tree.getRoot();
//        jObj = (JSONObject) jObj.get(jObj.getNames().iterator().next());

//        TypeMeta.Complex<Object> meta = info.getElement();

        String typeName = meta.getName();
        printLine("function " + typeName + "() {");
        indent++;

//        for (String name : jObj.getNames()) {
//            printLine("var _" + name + " = null;");
//        }
        printLine("var _data = " + (debug ? "this" : "{}") + ";");
        printLine("var _self = this;");
        printLine();

        for (TypeMeta.Property<Object, Object, Object> property : meta.getProperties()) {
            String name = property.getName();
            String type = property.getType().getName();
            if (property.getRelation() == RelationType.Collection) {
                type += "[]";
            }
            if (!property.getFlags().isReadOnly()) {
                printLine("/**");
                printLine(" * @param {" + type + "} value");
                printLine(" */");
                printLine("this.set" + upperFirst(name) + " = function (value) {");
                printLine("  _data." + name + " = value;");
                printLine("}");
            }
            printLine("/**");
            printLine(" * @type " + type);
            printLine(" */");
            printLine("this.get" + upperFirst(name) + " = function () {");
            printLine("  return _data." + name + ";");
            printLine("}");
        }
        printLine();

        printLine("this.toJSON = function() {");
        indent++;
        printLine("var result = {};");
        for (TypeMeta.Property<Object, Object, Object> property : meta.getProperties()) {
            String name = property.getName();
            if (!property.getFlags().isReadOnly()) {
                printLine("result." + name + " = _data." + name);
            }
        }
        printLine("return dojo.toJson(result);");
        indent--;
        printLine("}");
        printLine();

        if (collectionPath != null) {
            printLine("/**");
            printLine(" * @param {Function} _callback");
            printLine(" */");
            printLine("this.save = function(_callback) {");
            indent++;
            printLine("var data = {");
            indent++;
            printLine("handleAs: \"json\",");
            printLine("headers: { \"Content-Type\": \"application/json\"},");
            printLine("load: function (response, ioArgs) {");
//        printLine("  console.log(response);");
            printLine("  _callback((((typeof response) == \"object\") && (response != null)) ? new "
                    + typeName + "(response) : _self);");
            printLine("},");
            printLine("error: function (response, ioArgs) {");
            printLine("  console.error(response, ioArgs);");
            printLine("}");
            indent--;
            printLine("};");
            printLine("if (_data." + meta.getIdProperty().getName() + " == null) {");
            indent++;
            printLine("data.url = " + basePackage + "._root + \"" + apiPath + collectionPath
                    + ".json\";");
            printLine("data.postData = _self.toJSON();");
            printLine("dojo.xhrPost(data);");
            indent--;
            printLine("} else {");
            indent++;
            printLine("data.url = _data." + meta.getIdProperty().getName() + ";");
            printLine("data.putData = _self.toJSON();");
            printLine("dojo.xhrPut(data);");
            indent--;
            printLine("}");
            indent--;
            printLine("}");
            printLine();
        }

        printLine("if (arguments.length == 1 && (typeof arguments[0]) == \"object\") {");
        indent++;
        printLine("var d = arguments[0];");
        for (TypeMeta.Property<Object, Object, Object> property : meta.getProperties()) {
            String name = property.getName();
//            ElementRelation relation = getRelation(entity, name);
            if (! property.getType().isPrimitive()) {
                String relatedName = property.getType().getName();
                switch (property.getRelation()) {
                    case Collection: {
                        printLine("_data." + name + " = [];");
                        printLine("for (var i in d." + name + ") {");
                        indent++;
                        printLine("_data." + name + ".push(new " + relatedName
                                + "(d." + name + "[i]));");
                        indent--;
                        printLine("}");
                        break;
                    }
                    case Map: {
                        printLine("_data." + name + " = new Map_" + relatedName + "();");
                        printLine("for (var i in d." + name + ") {");
                        indent++;
                        printLine("_data." + name + ".put(i, new " + relatedName
                                + "(d." + name + "[i]));");
                        indent--;
                        printLine("}");
                        break;
                    }
                    case Single: {
                        printLine("_data." + name + " = new " + relatedName + "(d."
                                + name + ");");
                        break;
                    }
                    default:
                        throw new IllegalArgumentException(property.getRelation().name());
                }
            } else {
                printLine("_data." + name + " = d." + name + ";");
            }
        }
        indent--;
        printLine("} else {");
        indent++;
        for (TypeMeta.Property<Object, Object, Object> property : meta.getProperties()) {
            String name = property.getName();
//            ElementRelation relation = getRelation(entity, name);
            if ((property.getType() instanceof TypeMeta.Complex) && property.getRelation()
                    == RelationType.Collection) {
                printLine("_data." + name + " = [];");
            } else {
                printLine("_data." + name + " = null;");
            }
        }
        indent--;
        printLine("}");

        indent--;
        printLine("}");
        printLine();
    }

    private String upperFirst(String value) {
        return value.substring(0, 1).toUpperCase() + value.substring(1);
    }

    private void generateHandlerType(ResourceInfo info) throws IOException {
        printLine("function " + info.getHandlerTypeName() + "() {");
        indent++;

        {
//            RequestInfo load = info.getLoadRequest();
//            if (load != null) {
            printLine("/**");
            String methodParams = "href";
            printLine(" * @param {String} href");
//                for (Map.Entry<String, String> param : load.getJSParams().entrySet()) {
//                    methodParams = (methodParams == null ? "" : (methodParams + ", "))
//                            + param.getKey();
//                    printLine(" * @param {" + param.getValue() + "} " + param.getKey());
//                }
            printLine(" * @param {Function} _callback");
//            printLine(" * @return {" + entity.getEntityTypeName() + "}");
            printLine(" */");
            printLine("this.load = function (" + methodParams + ", _callback) {");
            indent++;

            printLine("dojo.xhrGet( {");
            indent++;
            printLine("url: href,");
            printLine("handleAs: \"json\",");
            printLine("load: function (response, ioArgs) {");
            printLine("  _callback(new " + info.getElement().getName() + "(response));");
            printLine("},");
            printLine("error: function (response, ioArgs) {");
            printLine("  console.error(response, ioArgs);");
            printLine("}");
            indent--;
            printLine("});");

            indent--;
            printLine("}");
//            }
        }

        {
//            RequestInfo list = info.getListRequest();
//            if (list != null) {
            printLine("/**");
//                String methodParams = null;
//                for (Map.Entry<String, String> param : list.getJSParams().entrySet()) {
//                    methodParams = (methodParams == null ? "" : (methodParams + ", "))
//                            + param.getKey();
//                    printLine(" * @param {" + param.getValue() + "} " + param.getKey());
//                }
            printLine(" * @param {Function} _callback");
            printLine(" */");
            printLine("this.list = function (_callback) {");
            indent++;

            printLine("dojo.xhrGet( {");
            indent++;
            printLine("url: " + basePackage + "._root + \"" + apiPath + info.getCollectionPath()
                    + ".json\",");
            printLine("handleAs: \"json\",");
            printLine("load: function (response, ioArgs) {");
            printLine("  var data = [];");
            printLine("  for (var i in response." + info.getCollectionElements() + ") {");
            printLine("    data.push(new " + info.getElement().getName() + "(response."
                    + info.getCollectionElements() + "[i]));");
            printLine("  }");
            printLine("  _callback(data);");
            printLine("},");
            printLine("error: function (response, ioArgs) {");
            printLine("  console.error(response, ioArgs);");
            printLine("}");
            indent--;
            printLine("});");

            indent--;
            printLine("}");
            printLine("/**");

//                methodParams = null;
//                for (Map.Entry<String, String> param : list.getJSParams().entrySet()) {
//                    methodParams = (methodParams == null ? "" : (methodParams + ", "))
//                            + param.getKey();
//                    printLine(" * @param {" + param.getValue() + "} " + param.getKey());
//                }
            printLine(" * @param {Function} _callback");
            printLine(" */");
            printLine("this.each = function (_callback) {");
            indent++;

            printLine("dojo.xhrGet( {");
            indent++;
            printLine("url: " + basePackage + "._root + \"" + apiPath + info.getCollectionPath()
                    + ".json\",");
            printLine("handleAs: \"json\",");
            printLine("load: function (response, ioArgs) {");
            printLine("  var length = response." + info.getCollectionElements() + ".length;");
            printLine("  for (var i in response." + info.getCollectionElements() + ") {");
            printLine("    _callback(new " + info.getElement().getName() + "(response."
                    + info.getCollectionElements() + "[i]), i, length);");
            printLine("  }");
            printLine("},");
            printLine("error: function (response, ioArgs) {");
            printLine("  console.error(response, ioArgs);");
            printLine("}");
            indent--;
            printLine("});");

            indent--;
            printLine("}");
//        }
        }

        indent--;
        printLine("}");
        printLine();
    }

    private void generateHandlerInstance(ResourceInfo entity) throws IOException {
        printLine(entity.getHandlerInstanceName() + " : new " + entity.getHandlerTypeName() + "(),");
        printLine();
    }

}
